﻿using Okra.Helpers;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml.Navigation;

namespace Okra.Navigation
{
    public abstract class NavigationBase : INavigationBase
    {
        // *** Fields ***

        private readonly IViewFactory viewFactory;
        private readonly INavigationStack navigationStack;
        private readonly NavigationContext navigationContext;

        private readonly Dictionary<INavigationEntry, PageDetails> pageCache = new Dictionary<INavigationEntry, PageDetails>();

        private bool restoringState = false;

        // *** Events ***

        public event EventHandler CanGoBackChanged;
        public event EventHandler<PageNavigationEventArgs> NavigatingFrom;
        public event EventHandler<PageNavigationEventArgs> NavigatedTo;

        // *** Constructors ***

        public NavigationBase(IViewFactory viewFactory)
            : this(viewFactory, new NavigationStack())
        {
        }

        public NavigationBase(IViewFactory viewFactory, INavigationStack navigationStack)
        {
            this.viewFactory = viewFactory;
            this.navigationStack = navigationStack;

            this.navigationContext = new NavigationContext(this);

            navigationStack.NavigatedTo += navigationStack_NavigatedTo;
            navigationStack.NavigatingFrom += navigationStack_NavigatingFrom;
            navigationStack.PageDisposed += navigationStack_PageDisposed;
            navigationStack.PropertyChanged += navigationStack_PropertyChanged;
        }

        // *** Properties ***

        public bool CanGoBack
        {
            get
            {
                return navigationStack.CanGoBack;
            }
        }

        public INavigationStack NavigationStack
        {
            get
            {
                return navigationStack;
            }
        }

        // *** Methods ***

        public bool CanNavigateTo(string pageName)
        {
            // Query the underlying view factory to see if the page exists

            return viewFactory.IsViewDefined(pageName);
        }

        public IEnumerable<object> GetPageElements(INavigationEntry page)
        {
            // Get the cached page if present, otherwise return an empty array

            PageDetails pageDetails;

            if (!pageCache.TryGetValue(page, out pageDetails))
                return new object[] { };

            IViewLifetimeContext viewLifetimeContext = pageDetails.ViewLifetimeContext;

            // Return in the following order,
            //   (1) The view model (if present)
            //   (2) The view

            
            if (viewLifetimeContext.ViewModel != null)
                return new object[] { viewLifetimeContext.ViewModel, viewLifetimeContext.View };
            else
                return new object[] { viewLifetimeContext.View };
        }

        // *** Protected Methods ***

        protected void CallNavigatedTo(INavigationEntry entry, NavigationMode navigationMode)
        {
            if (entry == null)
                return;

            // Fire the NavigatedTo event

            OnNavigatedTo(new PageNavigationEventArgs(entry, navigationMode));

            // Call NavigatedTo on all page elements

            foreach (object element in GetPageElements(entry))
            {
                if (element is INavigationAware)
                    ((INavigationAware)element).NavigatedTo(navigationMode);
            }
        }

        protected void CallNavigatingFrom(INavigationEntry entry, NavigationMode navigationMode)
        {
            if (entry == null)
                return;

            // Fire the NavigatingFrom event

            OnNavigatingFrom(new PageNavigationEventArgs(entry, navigationMode));

            // Call NavigatingFrom on all page elements

            foreach (object element in GetPageElements(entry))
            {
                if (element is INavigationAware)
                    ((INavigationAware)element).NavigatingFrom(navigationMode);
            }
        }

        protected void DisplayNavigationEntry(INavigationEntry entry)
        {
            if (entry == null)
            {
                // If this entry is null then simply pass null to the deriving class

                DisplayPage(null);
            }
            else
            {
                // If the page is in the cache then retrieve it, otherwise create the page and view-model

                PageDetails pageDetails;

                if (!pageCache.TryGetValue(entry, out pageDetails))
                    pageDetails = CreatePage((NavigationEntry)entry);

                // Navigate to the relevant page

                DisplayPage(pageDetails.ViewLifetimeContext.View);
            }
        }

        protected abstract void DisplayPage(object page);

        protected virtual void OnCanGoBackChanged()
        {
            EventHandler eventHandler = CanGoBackChanged;

            if (eventHandler != null)
                eventHandler(this, EventArgs.Empty);
        }

        protected virtual void OnNavigatingFrom(PageNavigationEventArgs args)
        {
            EventHandler<PageNavigationEventArgs> eventHandler = NavigatingFrom;

            if (eventHandler != null)
                eventHandler(this, args);
        }

        protected virtual void OnNavigatedTo(PageNavigationEventArgs args)
        {
            EventHandler<PageNavigationEventArgs> eventHandler = NavigatedTo;

            if (eventHandler != null)
                eventHandler(this, args);
        }

        protected void RestoreState(NavigationState state)
        {
            IEnumerable<INavigationEntry> pages = state.NavigationStack.Select(
                    entryState => new NavigationEntry(entryState.PageName, entryState.ArgumentsData, entryState.StateData));

            // Flag that we are restoring state so we can pass a NavigationMode.Refresh to restored pages
            // TODO : Determine if this is the best navigation mode to use?

            restoringState = true;

            try
            {
                NavigationStack.Push(pages);
            }
            finally
            {
                restoringState = false;
            }
        }

        protected NavigationState StoreState()
        {
            // Create an object for storage of the navigation state

            NavigationState state = new NavigationState();

            // Enumerate all NavigationEntries in the navigation stack

            foreach (NavigationEntry entry in NavigationStack)
            {
                // Save the page state
                // TODO : Do this when navigating away from each page to save time when suspending

                SavePageState(entry);

                // Create an object for storage of this entry

                NavigationEntryState entryState = new NavigationEntryState(entry.PageName, entry.ArgumentsData, entry.StateData);
                state.NavigationStack.Add(entryState);
            }

            // Return the result

            return state;
        }

        // *** Private Methods ***

        private PageDetails CreatePage(NavigationEntry entry)
        {
            // Create the View

            IViewLifetimeContext viewLifetimeContext = viewFactory.CreateView(entry.PageName, navigationContext);   

            // Activate the view model if it implements IActivatable<,>
            // NB: Use reflection as we do not know the generic parameter types

            object viewModel = viewLifetimeContext.ViewModel;
            Type activatableInterface = ReflectionHelper.GetClosedGenericType(viewModel, typeof(IActivatable<,>));

            if (activatableInterface != null)
            {
                // If required deserialize the arguments and state

                entry.DeserializeData(activatableInterface.GenericTypeArguments[0], activatableInterface.GenericTypeArguments[1]);

                // Activate the view model

                MethodInfo activateMethod = activatableInterface.GetTypeInfo().GetDeclaredMethod("Activate");
                activateMethod.Invoke(viewModel, new object[] { entry.Arguments, entry.State });
            }

            // Add the page details to the cache and return

            PageDetails pageDetails = new PageDetails(viewLifetimeContext);
            pageCache.Add(entry, pageDetails);

            return pageDetails;
        }

        private void SavePageState(NavigationEntry entry)
        {
            // If the view model is IActivatable<,> then use this to save the page state
            // NB: First check that the view has been created - this may still have state from a previous instance
            // NB: Use reflection as we do not know the generic parameter types

            PageDetails pageDetails;

            if (pageCache.TryGetValue(entry, out pageDetails))
            {
                // Get the generic IActivatable<,> interface

                object viewModel = pageDetails.ViewLifetimeContext.ViewModel;
                Type activatableInterface = ReflectionHelper.GetClosedGenericType(viewModel, typeof(IActivatable<,>));

                if (activatableInterface != null)
                {
                    // Save the state

                    MethodInfo saveStateMethod = activatableInterface.GetTypeInfo().GetDeclaredMethod("SaveState");
                    entry.State = saveStateMethod.Invoke(viewModel, null);

                    // Serialize the arguments and state

                    entry.SerializeData(activatableInterface.GenericTypeArguments[0], activatableInterface.GenericTypeArguments[1]);
                }
            }
        }

        // *** Event Handlers ***

        private void navigationStack_NavigatedTo(object sender, PageNavigationEventArgs e)
        {
            if (restoringState)
                CallNavigatedTo((INavigationEntry)e.Page, NavigationMode.Refresh);
            else
                CallNavigatedTo((INavigationEntry)e.Page, e.NavigationMode);
        }

        private void navigationStack_NavigatingFrom(object sender, PageNavigationEventArgs e)
        {
            CallNavigatingFrom((INavigationEntry)e.Page, e.NavigationMode);
        }

        private void navigationStack_PageDisposed(object sender, PageNavigationEventArgs e)
        {
            PageDetails pageDetails;

            if (pageCache.TryGetValue(e.Page, out pageDetails))
            {
                // Dispose of the view and view-model

                pageDetails.ViewLifetimeContext.Dispose();

                // Remove the page from the cache

                pageCache.Remove(e.Page);
            }
        }

        private void navigationStack_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            switch (e.PropertyName)
            {
                case "CanGoBack":
                    OnCanGoBackChanged();
                    break;
                case "CurrentPage":
                    DisplayNavigationEntry((INavigationEntry)navigationStack.CurrentPage);
                    break;
            }
        }

        // *** Private Sub-classes ***

        private class PageDetails
        {
            // *** Fields ***

            private readonly IViewLifetimeContext viewLifetimeContext;

            // *** Constructors ***

            public PageDetails(IViewLifetimeContext viewLifetimeContext)
            {
                this.viewLifetimeContext = viewLifetimeContext;
            }

            // *** Properties ***

            public IViewLifetimeContext ViewLifetimeContext
            {
                get
                {
                    return viewLifetimeContext;
                }
            }
        }
    }
}
